<!--
  - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
  - SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
	<div class="sharingTab" :class="{ 'icon-loading': loading }">
		<!-- error message -->
		<div v-if="error" class="emptycontent" :class="{ emptyContentWithSections: hasExternalSections }">
			<div class="icon icon-error" />
			<h2>{{ error }}</h2>
		</div>

		<!-- shares content -->
		<div
			v-show="!showSharingDetailsView"
			class="sharingTab__content">
			<!-- shared with me information -->
			<ul v-if="isSharedWithMe">
				<SharingEntrySimple v-bind="sharedWithMe" class="sharing-entry__reshare">
					<template #avatar>
						<NcAvatar
							:user="sharedWithMe.user"
							:display-name="sharedWithMe.displayName"
							class="sharing-entry__avatar" />
					</template>
				</SharingEntrySimple>
			</ul>

			<section>
				<div class="section-header">
					<h4>{{ t('files_sharing', 'Internal shares') }}</h4>
					<NcPopover popup-role="dialog">
						<template #trigger>
							<NcButton
								class="hint-icon"
								variant="tertiary-no-background"
								:aria-label="t('files_sharing', 'Internal shares explanation')">
								<template #icon>
									<InfoIcon :size="20" />
								</template>
							</NcButton>
						</template>
						<p class="hint-body">
							{{ internalSharesHelpText }}
						</p>
					</NcPopover>
				</div>
				<!-- add new share input -->
				<SharingInput
					v-if="!loading"
					:can-reshare="canReshare"
					:file-info="fileInfo"
					:link-shares="linkShares"
					:reshare="reshare"
					:shares="shares"
					:placeholder="internalShareInputPlaceholder"
					@open-sharing-details="toggleShareDetailsView" />

				<!-- other shares list -->
				<SharingList
					v-if="!loading"
					ref="shareList"
					:shares="shares"
					:file-info="fileInfo"
					@open-sharing-details="toggleShareDetailsView" />

				<!-- inherited shares -->
				<SharingInherited v-if="canReshare && !loading" :file-info="fileInfo" />

				<!-- internal link copy -->
				<SharingEntryInternal :file-info="fileInfo" />
			</section>

			<section>
				<div class="section-header">
					<h4>{{ t('files_sharing', 'External shares') }}</h4>
					<NcPopover popup-role="dialog">
						<template #trigger>
							<NcButton
								class="hint-icon"
								variant="tertiary-no-background"
								:aria-label="t('files_sharing', 'External shares explanation')">
								<template #icon>
									<InfoIcon :size="20" />
								</template>
							</NcButton>
						</template>
						<p class="hint-body">
							{{ externalSharesHelpText }}
						</p>
					</NcPopover>
				</div>
				<SharingInput
					v-if="!loading"
					:can-reshare="canReshare"
					:file-info="fileInfo"
					:link-shares="linkShares"
					:is-external="true"
					:placeholder="externalShareInputPlaceholder"
					:reshare="reshare"
					:shares="shares"
					@open-sharing-details="toggleShareDetailsView" />
				<!-- Non link external shares list -->
				<SharingList
					v-if="!loading"
					:shares="externalShares"
					:file-info="fileInfo"
					@open-sharing-details="toggleShareDetailsView" />
				<!-- link shares list -->
				<SharingLinkList
					v-if="!loading && isLinkSharingAllowed"
					ref="linkShareList"
					:can-reshare="canReshare"
					:file-info="fileInfo"
					:shares="linkShares"
					@open-sharing-details="toggleShareDetailsView" />
			</section>

			<section v-if="hasExternalSections && !showSharingDetailsView">
				<div class="section-header">
					<h4>{{ t('files_sharing', 'Additional shares') }}</h4>
					<NcPopover popup-role="dialog">
						<template #trigger>
							<NcButton
								class="hint-icon"
								variant="tertiary-no-background"
								:aria-label="t('files_sharing', 'Additional shares explanation')">
								<template #icon>
									<InfoIcon :size="20" />
								</template>
							</NcButton>
						</template>
						<p class="hint-body">
							{{ additionalSharesHelpText }}
						</p>
					</NcPopover>
				</div>
				<!-- additional entries, use it with cautious -->
				<SidebarTabExternalSection
					v-for="section in sortedExternalSections"
					:key="section.id"
					:section="section"
					:node="fileInfo.node /* TODO: Fix once we have proper Node API */"
					class="sharingTab__additionalContent" />

				<!-- legacy sections: TODO: Remove as soon as possible -->
				<SidebarTabExternalSectionLegacy
					v-for="(section, index) in legacySections"
					:key="index"
					:file-info="fileInfo"
					:section-callback="section"
					class="sharingTab__additionalContent" />

				<!-- projects (deprecated as of NC25 (replaced by related_resources) - see instance config "projects.enabled" ; ignore this / remove it / move into own section) -->
				<div
					v-if="projectsEnabled"
					v-show="!showSharingDetailsView && fileInfo"
					class="sharingTab__additionalContent">
					<NcCollectionList
						:id="`${fileInfo.id}`"
						type="file"
						:name="fileInfo.name" />
				</div>
			</section>
		</div>

		<!-- share details -->
		<SharingDetailsTab
			v-if="showSharingDetailsView"
			:file-info="shareDetailsData.fileInfo"
			:share="shareDetailsData.share"
			@close-sharing-details="toggleShareDetailsView"
			@add:share="addShare"
			@remove:share="removeShare" />
	</div>
</template>

<script>
import { getCurrentUser } from '@nextcloud/auth'
import axios from '@nextcloud/axios'
import { getCapabilities } from '@nextcloud/capabilities'
import { orderBy } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import moment from '@nextcloud/moment'
import { generateOcsUrl } from '@nextcloud/router'
import { ShareType } from '@nextcloud/sharing'
import { getSidebarSections } from '@nextcloud/sharing/ui'
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcCollectionList from '@nextcloud/vue/components/NcCollectionList'
import NcPopover from '@nextcloud/vue/components/NcPopover'
import InfoIcon from 'vue-material-design-icons/InformationOutline.vue'
import SharingEntryInternal from '../components/SharingEntryInternal.vue'
import SharingEntrySimple from '../components/SharingEntrySimple.vue'
import SharingInput from '../components/SharingInput.vue'
import SidebarTabExternalSection from '../components/SidebarTabExternal/SidebarTabExternalSection.vue'
import SidebarTabExternalSectionLegacy from '../components/SidebarTabExternal/SidebarTabExternalSectionLegacy.vue'
import SharingDetailsTab from './SharingDetailsTab.vue'
import SharingInherited from './SharingInherited.vue'
import SharingLinkList from './SharingLinkList.vue'
import SharingList from './SharingList.vue'
import ShareDetails from '../mixins/ShareDetails.js'
import Share from '../models/Share.ts'
import Config from '../services/ConfigService.ts'
import logger from '../services/logger.ts'
import { shareWithTitle } from '../utils/SharedWithMe.js'

const productName = window.OC.theme.productName

export default {
	name: 'SharingTab',

	components: {
		InfoIcon,
		NcAvatar,
		NcButton,
		NcCollectionList,
		NcPopover,
		SharingEntryInternal,
		SharingEntrySimple,
		SharingInherited,
		SharingInput,
		SharingLinkList,
		SharingList,
		SharingDetailsTab,
		SidebarTabExternalSection,
		SidebarTabExternalSectionLegacy,
	},

	mixins: [ShareDetails],

	data() {
		return {
			config: new Config(),
			deleteEvent: null,
			error: '',
			expirationInterval: null,
			loading: true,

			fileInfo: null,

			// reshare Share object
			reshare: null,
			sharedWithMe: {},
			shares: [],
			linkShares: [],
			externalShares: [],

			legacySections: OCA.Sharing.ShareTabSections.getSections(),
			sections: getSidebarSections(),

			projectsEnabled: loadState('core', 'projects_enabled', false),
			showSharingDetailsView: false,
			shareDetailsData: {},
			returnFocusElement: null,

			internalSharesHelpText: t('files_sharing', 'Share files within your organization. Recipients who can already view the file can also use this link for easy access.'),
			externalSharesHelpText: t('files_sharing', 'Share files with others outside your organization via public links and email addresses. You can also share to {productName} accounts on other instances using their federated cloud ID.', { productName }),
			additionalSharesHelpText: t('files_sharing', 'Shares from apps or other sources which are not included in internal or external shares.'),
		}
	},

	computed: {
		/**
		 * Are any sections registered by other apps.
		 *
		 * @return {boolean}
		 */
		hasExternalSections() {
			return this.sections.length > 0 || this.legacySections.length > 0
		},

		sortedExternalSections() {
			return this.sections
				.filter((section) => section.enabled(this.fileInfo.node))
				.sort((a, b) => a.order - b.order)
		},

		/**
		 * Is this share shared with me?
		 *
		 * @return {boolean}
		 */
		isSharedWithMe() {
			return !!this.sharedWithMe?.user
		},

		/**
		 * Is link sharing allowed for the current user?
		 *
		 * @return {boolean}
		 */
		isLinkSharingAllowed() {
			const currentUser = getCurrentUser()
			if (!currentUser) {
				return false
			}

			const capabilities = getCapabilities()
			const publicSharing = capabilities.files_sharing?.public || {}
			return publicSharing.enabled === true
		},

		canReshare() {
			return !!(this.fileInfo.permissions & OC.PERMISSION_SHARE)
				|| !!(this.reshare && this.reshare.hasSharePermission && this.config.isResharingAllowed)
		},

		internalShareInputPlaceholder() {
			return this.config.showFederatedSharesAsInternal && this.config.isFederationEnabled
				// TRANSLATORS: Type as in with a keyboard
				? t('files_sharing', 'Type names, teams, federated cloud IDs')
				// TRANSLATORS: Type as in with a keyboard
				: t('files_sharing', 'Type names or teams')
		},

		externalShareInputPlaceholder() {
			if (!this.isLinkSharingAllowed) {
				// TRANSLATORS: Type as in with a keyboard
				return this.config.isFederationEnabled ? t('files_sharing', 'Type a federated cloud ID') : ''
			}
			return !this.config.showFederatedSharesAsInternal && !this.config.isFederationEnabled
				// TRANSLATORS: Type as in with a keyboard
				? t('files_sharing', 'Type an email')
				// TRANSLATORS: Type as in with a keyboard
				: t('files_sharing', 'Type an email or federated cloud ID')
		},
	},

	methods: {
		/**
		 * Update current fileInfo and fetch new data
		 *
		 * @param {object} fileInfo the current file FileInfo
		 */
		async update(fileInfo) {
			this.fileInfo = fileInfo
			this.resetState()
			this.getShares()
		},

		/**
		 * Get the existing shares infos
		 */
		async getShares() {
			try {
				this.loading = true

				// init params
				const shareUrl = generateOcsUrl('apps/files_sharing/api/v1/shares')
				const format = 'json'
				// TODO: replace with proper getFUllpath implementation of our own FileInfo model
				const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')

				// fetch shares
				const fetchShares = axios.get(shareUrl, {
					params: {
						format,
						path,
						reshares: true,
					},
				})
				const fetchSharedWithMe = axios.get(shareUrl, {
					params: {
						format,
						path,
						shared_with_me: true,
					},
				})

				// wait for data
				const [shares, sharedWithMe] = await Promise.all([fetchShares, fetchSharedWithMe])
				this.loading = false

				// process results
				this.processSharedWithMe(sharedWithMe)
				this.processShares(shares)
			} catch (error) {
				if (error?.response?.data?.ocs?.meta?.message) {
					this.error = error.response.data.ocs.meta.message
				} else {
					this.error = t('files_sharing', 'Unable to load the shares list')
				}
				this.loading = false
				logger.error('Error loading the shares list', error)
			}
		},

		/**
		 * Reset the current view to its default state
		 */
		resetState() {
			clearInterval(this.expirationInterval)
			this.loading = true
			this.error = ''
			this.sharedWithMe = {}
			this.shares = []
			this.linkShares = []
			this.externalShares = []
			this.showSharingDetailsView = false
			this.shareDetailsData = {}
		},

		/**
		 * Update sharedWithMe.subtitle with the appropriate
		 * expiration time left
		 *
		 * @param {Share} share the sharedWith Share object
		 */
		updateExpirationSubtitle(share) {
			const expiration = moment(share.expireDate).unix()
			this.$set(this.sharedWithMe, 'subtitle', t('files_sharing', 'Expires {relativetime}', {
				relativetime: moment(expiration * 1000).fromNow(),
			}))

			// share have expired
			if (moment().unix() > expiration) {
				clearInterval(this.expirationInterval)
				// TODO: clear ui if share is expired
				this.$set(this.sharedWithMe, 'subtitle', t('files_sharing', 'this share just expired.'))
			}
		},

		/**
		 * Process the current shares data
		 * and init shares[]
		 *
		 * @param {object} share the share ocs api request data
		 * @param {object} share.data the request data
		 */
		processShares({ data }) {
			if (data.ocs && data.ocs.data && data.ocs.data.length > 0) {
				const shares = orderBy(
					data.ocs.data.map((share) => new Share(share)),
					[
						// First order by the "share with" label
						(share) => share.shareWithDisplayName,
						// Then by the label
						(share) => share.label,
						// And last resort order by createdTime
						(share) => share.createdTime,
					],
				)

				for (const share of shares) {
					if ([ShareType.Link, ShareType.Email].includes(share.type)) {
						this.linkShares.push(share)
					} else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) {
						if (this.config.showFederatedSharesToTrustedServersAsInternal) {
							if (share.isTrustedServer) {
								this.shares.push(share)
							} else {
								this.externalShares.push(share)
							}
						} else if (this.config.showFederatedSharesAsInternal) {
							this.shares.push(share)
						} else {
							this.externalShares.push(share)
						}
					} else {
						this.shares.push(share)
					}
				}

				logger.debug(`Processed ${this.linkShares.length} link share(s)`)
				logger.debug(`Processed ${this.shares.length} share(s)`)
				logger.debug(`Processed ${this.externalShares.length} external share(s)`)
			}
		},

		/**
		 * Process the sharedWithMe share data
		 * and init sharedWithMe
		 *
		 * @param {object} share the share ocs api request data
		 * @param {object} share.data the request data
		 */
		processSharedWithMe({ data }) {
			if (data.ocs && data.ocs.data && data.ocs.data[0]) {
				const share = new Share(data)
				const title = shareWithTitle(share)
				const displayName = share.ownerDisplayName
				const user = share.owner

				this.sharedWithMe = {
					displayName,
					title,
					user,
				}
				this.reshare = share

				// If we have an expiration date, use it as subtitle
				// Refresh the status every 10s and clear if expired
				if (share.expireDate && moment(share.expireDate).unix() > moment().unix()) {
					// first update
					this.updateExpirationSubtitle(share)
					// interval update
					this.expirationInterval = setInterval(this.updateExpirationSubtitle, 10000, share)
				}
			} else if (this.fileInfo && this.fileInfo.shareOwnerId !== undefined ? this.fileInfo.shareOwnerId !== getCurrentUser().uid : false) {
				// Fallback to compare owner and current user.
				this.sharedWithMe = {
					displayName: this.fileInfo.shareOwner,
					title: t(
						'files_sharing',
						'Shared with you by {owner}',
						{ owner: this.fileInfo.shareOwner },
						undefined,
						{ escape: false },
					),

					user: this.fileInfo.shareOwnerId,
				}
			}
		},

		/**
		 * Add a new share into the shares list
		 * and return the newly created share component
		 *
		 * @param {Share} share the share to add to the array
		 * @param {Function} [resolve] a function to run after the share is added and its component initialized
		 */
		addShare(share, resolve = () => { }) {
			// only catching share type MAIL as link shares are added differently
			// meaning: not from the ShareInput
			if (share.type === ShareType.Email) {
				this.linkShares.unshift(share)
			} else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) {
				if (this.config.showFederatedSharesAsInternal) {
					this.shares.unshift(share)
				}
				if (this.config.showFederatedSharesToTrustedServersAsInternal) {
					if (share.isTrustedServer) {
						this.shares.unshift(share)
					}
				} else {
					this.externalShares.unshift(share)
				}
			} else {
				this.shares.unshift(share)
			}
			this.awaitForShare(share, resolve)
		},

		/**
		 * Remove a share from the shares list
		 *
		 * @param {Share} share the share to remove
		 */
		removeShare(share) {
			// Get reference for this.linkShares or this.shares
			const shareList
				= share.type === ShareType.Email
					|| share.type === ShareType.Link
					? this.linkShares
					: this.shares
			const index = shareList.findIndex((item) => item.id === share.id)
			if (index !== -1) {
				shareList.splice(index, 1)
			}
		},

		/**
		 * Await for next tick and render after the list updated
		 * Then resolve with the matched vue component of the
		 * provided share object
		 *
		 * @param {Share} share newly created share
		 * @param {Function} resolve a function to execute after
		 */
		awaitForShare(share, resolve) {
			this.$nextTick(() => {
				let listComponent = this.$refs.shareList
				// Only mail shares comes from the input, link shares
				// are managed internally in the SharingLinkList component
				if (share.type === ShareType.Email) {
					listComponent = this.$refs.linkShareList
				}
				const newShare = listComponent.$children.find((component) => component.share === share)
				if (newShare) {
					resolve(newShare)
				}
			})
		},

		toggleShareDetailsView(eventData) {
			if (!this.showSharingDetailsView) {
				const isAction = Array.from(document.activeElement.classList)
					.some((className) => className.startsWith('action-'))
				if (isAction) {
					const menuId = document.activeElement.closest('[role="menu"]')?.id
					this.returnFocusElement = document.querySelector(`[aria-controls="${menuId}"]`)
				} else {
					this.returnFocusElement = document.activeElement
				}
			}

			if (eventData) {
				this.shareDetailsData = eventData
			}

			this.showSharingDetailsView = !this.showSharingDetailsView

			if (!this.showSharingDetailsView) {
				this.$nextTick(() => { // Wait for next tick as the element must be visible to be focused
					this.returnFocusElement?.focus()
					this.returnFocusElement = null
				})
			}
		},
	},
}
</script>

<style scoped lang="scss">
.emptyContentWithSections {
	margin: 1rem auto;
}

.sharingTab {
	position: relative;
	height: 100%;

	&__content {
		padding: 0 6px;

		section {
			padding-bottom: 16px;

			.section-header {
				margin-top: 2px;
				margin-bottom: 2px;
				display: flex;
				align-items: center;
				padding-bottom: 4px;

				h4 {
					margin: 0;
					font-size: 16px;
				}

				.visually-hidden {
					display: none;
				}

				.hint-icon {
					color: var(--color-primary-element);
				}

			}

		}

		& > section:not(:last-child) {
			border-bottom: 2px solid var(--color-border);
		}

	}

	&__additionalContent {
		margin: var(--default-clickable-area) 0;
	}
}

.hint-body {
	max-width: 300px;
	padding: var(--border-radius-element);
}
</style>
